#pragma once
#include "Helper.hpp"
#include "Location.hpp"
#include <string>
#include <unordered_map>
#include <memory>

namespace Welp
{
  using namespace std;

  class Variant;

  class VariantHasher
  {
  public:
    size_t operator()(const Variant& variant) const;
  };

  class VariantEqualTo
  {
  public:
    bool operator()(const Variant& a, const Variant& b) const;
  };

  namespace VariantType
  {
    enum Enum
    {
      Null,
      Number,
      String,
      Pointer,
      Function,
      Table
    };
  }

  class VirtualMachine;
  class FunctionNode;

  typedef Variant (*VariantMethod)(Variant& functionAndSelf, VirtualMachine* vm, const vector<Variant>& arguments);
  
  typedef unordered_map<Variant, Variant, VariantHasher, VariantEqualTo> Table;

  // The variant can basically store 'any' value (primitives such as numbers, strings, objects, etc)
  class Variant
  {
  public:

    // The type of value we're storing here
    VariantType::Enum mType;

    // Any kind of number, including integers, bools, characters, bytes, (signed and unsigned... etc!)
    double mNumber;
    string mString;
    void* mPointer;

    // If the variant is a function, then if it represents a non-static delegate 
    // the mPointer or mTable fields will be set (mTable should be set for the garbage collector)
    VariantMethod mFunction;
    FunctionNode* mFunctionNode;

    // A table allows any type of key to map to any type of value
    shared_ptr<Table> mTable;

    Variant();
    Variant(VariantType::Enum type);
    Variant(double value);
    Variant(void* value);
    Variant(const string& value);
    Variant(const char* value);
    Variant(VariantMethod function, Variant* self = nullptr);
    Variant(FunctionNode* functionNode, Variant* self = nullptr);

    void Clear();

    bool AsBool(const Location* location) const;

    // Convert the variant into a string
    string ValueToString() const;
    string TypeToString() const;

    // Call operation
    // Only works on user functions, node functions, or tables that expose a call operator (all else throws!)
    // The variant call will create a locals table (unless the called function is a closure, which it then uses its own locals)
    // If the locals table passed in is null, it will also force creation of a locals table
    // Returns null if no value is expected
    Variant Call(VirtualMachine* vm, const vector<Variant>& arguments, const Location* location, Variant* localsTable = nullptr);

    // Table operations
    // These will throw if the variant itself is not a table
    void InsertOverwrite(const Variant& key, const Variant& value, const Location* location);
    Variant& GetValue(const Variant& key, const Location* location);
    Variant& GetValueOrDefault(const Variant& key, Variant& defaultValue, const Location* location);
    bool HasValue(const Variant& key, const Location* location);
    size_t GetCount(const Location* location);

    // Binary operators
    // Exceptions may be thrown if the input parameters are not valid
    // The locations may be null, but if provided they will be attached to the exception
    static Variant Add         (const Variant& lhs, const Variant& rhs, const Location* location);
    static Variant Subtract    (const Variant& lhs, const Variant& rhs, const Location* location);
    static Variant Multiply    (const Variant& lhs, const Variant& rhs, const Location* location);
    static Variant Divide      (const Variant& lhs, const Variant& rhs, const Location* location);
    static Variant Modulo      (const Variant& lhs, const Variant& rhs, const Location* location);
    static Variant Exponent    (const Variant& lhs, const Variant& rhs, const Location* location);
    static Variant Concatenate (const Variant& lhs, const Variant& rhs, const Location* location);
  };
}